<?php
/**
 *
 * attributes
 * @property  string $created
 * @property  string $updated
 *
 */
abstract class Node {
  private $data = array();
  private $_load = false;

  public $_forceChangeCreated;

  const READ_ONLY = 'r';
  const WRITABLE = 'w';
  const WRITE_ONLY = 'wo';

  public static function option() {
    throw new Exception('need implement option');
  }

  public function getHashId() {
    $class = get_class($this);
    if (!isset($class::$hashPrefix) || !$class::$hashPrefix) return $this->id;
    $hashId = dec2hash($this->id * substr($this->created, -5), 7);
    return $class::$hashPrefix . '_' . $hashId . dec2hash($this->created, 8);
  }

  public static function loadByHashId($hashId) {
    $class = get_called_class();
    if (!isset($class::$hashPrefix)) return NULL;

    $prefix = $class::$hashPrefix;
    $prefixLength = strlen($prefix);
    $_ids = $hashId;
    if (!is_array($_ids)) {
      $_ids = [$_ids];
    }

    try {
      $ids = [];
      foreach ($_ids as $_hashId) {
        if (substr($_hashId, 0, $prefixLength + 1) != $prefix . '_') continue;

        $_id = substr($_hashId, $prefixLength + 1);
        $time = hash2dec(substr($_id, -6), 8);
        $_id = hash2dec(substr($_id, 0, -6), 7);
        $id = $_id / substr($time, -5);
        $ids[$_hashId] = $id;
      }

      if (is_array($hashId)) {
        $objs = self::load($ids);
        $data = [];
        $_id_map = array_flip($ids);
        foreach ($objs as $obj) {
          $data[$_id_map[$obj->id]] = $obj;
        }
        return $data;

      } else {
        return self::load(reset($ids));
      }

    } catch (Exception $e) {
      return is_array($hashId) ? [] : NULL;
    }
  }

  //hooks
  protected function after_load() {
    $this->created = strtotime($this->created);
    $this->updated = strtotime($this->updated);
    if (strlen($this->created) < 6) {
      $this->created = $this->updated;
    }
    $this->_load = true;
  }

  protected function hashId($prefix = '', $more = false, $offset = 0) {
    $uid = str_replace('.', '', uniqid('', $more));
    return $prefix . hex2hash($uid, $offset);
  }

  private static $storage;

  /**
   * @return Storage
   */
  protected static function storage() {
    $class_name = get_called_class();
    if (!isset(self::$storage[$class_name])) {
      self::$storage[$class_name] = self::getStorageWithOption($class_name::option());
    }
    return self::$storage[$class_name];
  }
  private static function getStorageWithOption($option) {
    /*$class_name = get_called_class();

    if (in_array($class_name, self::$cacheType) && !IS_CLI) {
      return new CachedStorage($option);
    }*/
    return new Storage($option);
  }

  public static function clear() {
    self::$cache = array();
  }

  private static $cache = array();
  private static function getStaticCache($ids) {
    $class = get_called_class();
    if (!isset(self::$cache[$class])) self::$cache[$class] = array();
    return array_intersect_key(self::$cache[$class], $ids);
  }

  private static function removeStaticCache($ids) {
    $class = get_called_class();
    foreach ($ids as $id) {
      unset(self::$cache[$class][$id]);
    }
  }

  private static function setStaticCache($nodes) {
    if (IS_CLI) return;
    $class = get_called_class();
    if (!isset(self::$cache[$class])) self::$cache[$class] = array();
    self::$cache[$class] = $nodes + self::$cache[$class];
  }

  public static function load($id) {
    if (!$id) return array();
    $ids = is_array($id) ? array_filter($id) : array($id);
    $ids = array_combine($ids, $ids);

    $cached_data = self::getStaticCache($ids);
    $storage_data = self::storage()->load(array_diff_key($ids, $cached_data));
    $caching_data = $return = array();
    $class_name = get_called_class();
    foreach ($ids as $_id) {
      if (isset($cached_data[$_id])) {
        $return[$_id] = $cached_data[$_id];
        continue;
      }

      if (!isset($storage_data[$_id])) continue;
      /**
       * @var Node $node
       */
      $node = new $class_name();
      foreach ($storage_data[$_id] as $key => $value) {
        $node->$key = $value;
      }
      $node->after_load();
      $caching_data[$_id] = $return[$_id] = $node;
    }

    if (count($caching_data) < 300) {
      self::setStaticCache($caching_data);
    }

    return is_array($id) ? $return : (current($return) ?: NULL);
  }

  final public static function loadBy($key, $value) {
    $data = self::storage()->loadBy($key, $value);
    if (!$data) return null;

    $class_name = get_called_class();
    $options = $class_name::option();
    $_id = $data[$options['key']];

    //static cache
    if (isset(self::$cache[$class_name][$_id])) return self::$cache[$class_name][$_id];

    /**
     * @var Node $node
     */
    $node = new $class_name();
    foreach ($data as $key => $value) {
      $node->$key = $value;
    }
    $node->after_load();
    self::$cache[$class_name][$_id] = $node;

    return $node;
  }

  final public static function findOne(BaseQuery $query = null) {
    return ($rs = self::find($query, array('size' => 1))) ? current($rs) : null;
  }

  final public static function totalCount(BaseQuery $query = null, Array $opts = array()) {
    return self::storage()->totalCount($query ?: new TrueQuery(), $opts);
  }

  /**
   * @param BaseQuery $query
   * @param array $option contains "size", "from", "sort"
   * @return array|mixed
   */
  final public static function find(BaseQuery $query = null,
                                    $option = array('from' => 0, 'size' => 10, 'sort' => array(), 'group' => '')) {
    return self::load(self::findIds($query, $option));
  }

  /**
   * @param BaseQuery $query
   * @param array $option contains "size", "from", "sort"
   * @return array|mixed
   */
  final public static function findIds(BaseQuery $query = null,
                                       $option = array('from' => 0, 'size' => 10, 'sort' => array())) {
    return self::storage()->find($query ?: new TrueQuery(), $option);
  }

  final public static function raw_fetch($raw_field, BaseQuery $query = null,
                                         $option = array('from' => 0, 'size' => 10, 'sort' => array())) {
    return self::storage()->raw_fetch($raw_field, $query ?: new TrueQuery(), $option);
  }

  public function save($flag = null) {
    if (!$flag) {
      $flag = ($this->_load ? Storage::SAVE_UPDATE : Storage::SAVE_INSERT);
    }

    $this->updated = time();
    if ($flag == Storage::SAVE_INSERT) {
      $this->created = $this->updated;
    }

    if (strlen($this->created) < 6) {
      Log::set('time_error', print_r(debug_backtrace(), true));
    }

    $this->created = timetostr($this->created);
    $this->updated = timetostr($this->updated);

    $data = $this->data;
    if ($flag == Storage::SAVE_UPDATE && !$this->_forceChangeCreated && isset($data['created'])) {
      unset($data['created']);
    }

    $id = self::storage()->save($data, $flag);
    $opt = $this->_opt();
    $key = $opt['key'];
    if ($id) {
      if (!$id) {
        throw new Exception('save fail');
      }

      if (!$this->$key) $this->$key = $id;
    }

    $this->after_load();
    self::removeStaticCache(array($id));//for safe

    $this->build();
  }

  public function delete() {
    $opt = $this->_opt();
    $key = $opt['key'];

    return self::delete_by_ids(array($this->$key));
  }

  public static function multi_delete(BaseQuery $query, $option = array('from' => 0, 'size' => 10, 'sort' => array())) {
    $ids = self::storage()->find($query, $option, false);
    return self::delete_by_ids($ids);
  }

  public static function delete_by_ids(Array $ids) {
    self::removeStaticCache($ids);
    self::storage()->delete($ids);

    $class_name = get_called_class();
    $opt = $class_name::option();
    if (isset($opt['search']) && $opt['search']) {
      foreach ($ids as $id) {
        (new Queue('BuildSearch'))->push(['id' => $id, 'type' => $opt['search'], 'del' => '1']);
      }
    }

    return count($ids);
  }

  public static function or_mapping() {
    $class_name = get_called_class();
    $opt = $class_name::option();
    return $opt['columns'];
  }

  //todo: make it private and call by multi_delete() checked cache strategy and dispatched
  public static function quick_update(BaseQuery $query, Array $new_data, Array $opts = array()) {
    $class_name = get_called_class();
    $opt = $class_name::option();
    if (isset($opt['search']) && $opt['search']) {
      throw new Exception('Searchable can not be quick update');
    }

    /*
     * may cause cache problem
     */
    if (in_array($class_name, self::$cacheType)) {
      throw new Exception('could not quick update cached nodes');
    }
    $storage = new Storage($class_name::option());
    $storage->quick_update($query, $new_data, $opts);
  }

  public static function quick_delete(BaseQuery $query, Array $opts = array()) {
    $class_name = get_called_class();
    $opt = $class_name::option();
    if (isset($opt['search']) && $opt['search']) {
      throw new Exception('Searchable can not be quick delete');
    }

    if (in_array($class_name, self::$cacheType)) {
      throw new Exception('could not quick delete cached nodes');
    }
    $storage = new Storage($class_name::option());
    return $storage->quick_delete($query, $opts);
  }

  public function attributes() {
    return $this->data;
  }

  public function __get($name) {
    return isset($this->data[$name]) ? strval($this->data[$name]) : '';//todo:should be null?
  }

  public function __set($name, $value) {
    $this->data[$name] = is_null($value) ? null : strval($value);
  }

  public function fields() {
    $keys = array_keys(self::or_mapping());
    $values = array_fill(0, count($keys), self::READ_ONLY);
    return array_combine($keys, $values);
  }

  //for search
  public function build() {
    $opt = $this->_opt();
    if (!isset($opt['search']) || !$opt['search']) return;
    $doc = $this->doc();
    if (!$doc) return;
    (new Queue('BuildSearch'))->push(['doc' => $doc, 'class' => get_class($this)]);
  }

  private $_opt = [];
  private function _opt() {
    if ($this->_opt) return $this->_opt;
    $class_name = get_class($this);
    $this->_opt = $class_name::option();
    return $this->_opt;
  }

  public function doc() {
    return [];
  }

  //attributes to read from HTTP request
  public static function attributesList() {
    return [];
  }

  public static function createByRequest(Request $request) {
    $class_name = get_called_class();
    $objAttributesList = $class_name::attributesList();
    $object = new $class_name();
    if (in_array('id', $objAttributesList) && $request->id) {
      $object = $class_name::load($request->id);
    }
    foreach ($objAttributesList as $attribute) {
      if(isset($attribute) && !is_null($attribute) && $attribute != 'id') {
        $object->$attribute = $request->$attribute;
      }
    }
    return $object;
  }
}